package com.nulldev.util.web.YouTubeExtractor.subtitles;

import java.util.List;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

import com.nulldev.util.JVM.Arguments;
import com.nulldev.util.VariableAPI.MathUtil;
import com.nulldev.util.VariableAPI.MathUtil.TimeUtil;
import com.nulldev.util.VariableAPI.StringsUtil;
import com.nulldev.util.VariableAPI.util.strings.splitters.Splitters;
import com.nulldev.util.concurrency.threadIt.v4.emapi.ExecutorManager;
import com.nulldev.util.concurrency.threadIt.v4.emapi.IExecutorManager;
import com.nulldev.util.concurrency.threadIt.v4.emapi.uft.Executable;
import com.nulldev.util.concurrency.threadIt.v4.executors.DATE;
import com.nulldev.util.io.IOUtils;
import com.nulldev.util.logging.Logger;
import com.nulldev.util.logging.LoggerFactory;
import com.nulldev.util.web.YouTubeExtractor.YouTubeExtractor;
import com.nulldev.util.web.YouTubeExtractor.subtitles.Subtitle.SubtitleStyle;

public class WebVTTBuilder {
	/**
	 * 1.0.9 [WIP] <br />
	 * Allow subtitle timeouts. <br />
	 * Tweak executor logic. <br />
	 * Added a workaround option for IE 11 and similar browsers which require less
	 * millisecond timestamp precision. <br />
	 * 1.0.8 <br />
	 * Support more formatting types. <br />
	 * Performance improvements. <br />
	 * 1.0.7: <br />
	 * Fixed timings. <br />
	 * Improved positioning. <br />
	 * Added experimental formatting support. <br />
	 * 1.0.6: <br />
	 * Added position table support. <br />
	 * Fixed horizontal positions. <br />
	 * Updated subtitle position logic. <br />
	 * 1.0.5: <br />
	 * Updated character decoding. <br />
	 * 1.0.4: <br />
	 * Added positional support. <br />
	 * 1.0.3: <br />
	 * add(String, double, double) is now add(Subtitle) <br />
	 * 1.0.2: <br />
	 * Updated to nullUtil 3.0 and added character decoding. <br />
	 * 1.0.1: <br />
	 * Bug fixes. <br />
	 * 1.0.0: <br />
	 * Initial implementation.
	 * 
	 * TODO: (For 1.0.9) - Merge the subtitle buffer append code.
	 */
	public static final String VERSION = "1.0.9";

	private static final Logger log = LoggerFactory.getLogger();
	private static final boolean USE_OLD_POS_LOGIC = Arguments.hasArgument("--WebVTTBuilder.useOldPosLogic");
	private static final boolean ENABLE_POSITIONAL_TWEAKS = Arguments.notArguments("--WebVTTBuilder.disablePosTweaks");

	protected static final IExecutorManager APPEND_ACCELERATOR = ExecutorManager.get().isForking() || ExecutorManager.get().executor() instanceof DATE
			? ExecutorManager.get()
			: new ExecutorManager(Executors.newWorkStealingPool(Runtime.getRuntime().availableProcessors() * 4));

	private final boolean enableLegacySpecWorkaround;
	private final boolean disablePositioning;

	final StringBuilder builder;
	private int index = 1;
	private final int emptyIndex;
	private boolean hasStyles;
	private boolean ignoreStyleAppend;
	private int stylesIndex = -1;

	public WebVTTBuilder() {
		this(false, false);
	}

	public WebVTTBuilder(final boolean enableLegacySpecWorkaround, final boolean disablePositioning) {
		this.disablePositioning = disablePositioning;
		this.enableLegacySpecWorkaround = enableLegacySpecWorkaround;
		this.builder = new StringBuilder(IOUtils.STRING_ALLOC_BUFFER);
		this.builder.append("WEBVTT\n\n");
		if (!enableLegacySpecWorkaround) {
			/* IE-11 hates anything involving NOTE. */
			this.builder.append("NOTE YTE=" + YouTubeExtractor.VERSION + ",WebVTTBuilder=" + VERSION + "\n\n");
		}
		this.emptyIndex = this.builder.length();
	}

	public void add(final Subtitle subtitle) {
		if (subtitle == null)
			return;
		final String text = subtitle.text();
		final float from = subtitle.start();
		final float to = subtitle.end();
		final boolean treatAsMills = subtitle.treatAsMilliseconds();
		if (text == null) {
			log.error("add(): Blank entry was skipped! [text=" + text + ",from=" + from + ",to=" + to + "]");
			return;
		}
		final Executable<?> f = ExecutorManager.make(() -> {
			if (subtitle.hasStyles() && !this.ignoreStyleAppend) {
				if (subtitle.getQualifiyingStyles() > 0) {
					if (!this.hasStyles) {
						this.builder.insert(this.emptyIndex, "STYLE\n");
						this.stylesIndex = this.builder.length();
						this.builder.append("\n");
						this.hasStyles = true;
					} else {
						for (final SubtitleStyle style : subtitle.getStyles()) {
							if (!style.qualifies())
								continue;
							else {
								int nl = this.stylesIndex;
								final String p = "::cue(." + style.textIdf + "), c[class=\"" + style.textIdf + "\"] {\n";
								this.builder.insert(this.stylesIndex, p);
								nl += p.length();
								if (style.hasColor()) {
									final String c = "color: #" + MathUtil.toHex(style.color) + ";\n";
									this.builder.insert(nl, c);
									nl += c.length();
								}
								if (style.hasBackgroundAlpha()) {
									if (style.backgroundAlpha == 0) {
										final String c = "background-color: transparent;\n";
										this.builder.insert(nl, c);
										nl += c.length();
									} else if (style.backgroundAlpha == 254 || style.backgroundAlpha == 255) {
									} else
										log.warn("Unsupported backgroundAlpha: " + style.backgroundAlpha);
								}
								if (style.hasForegroundAlpha()) {
									final String c = "opacity: " + (float) ((float) style.foregroundAlpha / 254) + ";\n";
									this.builder.insert(nl, c);
									nl += c.length();
								}
								if (style.hasTextSizeIncrement()) {
									final String c = "font-size: " + (float) (100 * style.textSizeIncrement) + "%;\n";
									this.builder.insert(nl, c);
									nl += c.length();
								}
								if (style.hasEdgeColor()) {
									final String color = "#" + MathUtil.toHex(style.edgeColor);
									final String c = "text-shadow: -1.1px -1.1px 0 " + color + ", 1.1px -1.1px 0 " + color + ", -1.1px 1.1px 0 " + color
											+ ", 1.1px 1.1px 0 " + color + ";\n";
									this.builder.insert(nl, c);
									nl += c.length();
								}
								if (style.hasFontFamily()) {
									final String c = (style.fontFamily == 7
											? "font-variant: small-caps;\nfont-family: " + SubtitleStyle.getFontFamilyForInt(style.fontFamily)
											: "font-family: " + SubtitleStyle.getFontFamilyForInt(style.fontFamily)) + ";\n";
									this.builder.insert(nl, c);
									nl += c.length();
								}
								this.builder.insert(nl, "}\n");
							}
						}
					}
				}
			}
			this.builder.append(this.index + "\n");
			final String c0 = treatAsMills ? convertNative(from, enableLegacySpecWorkaround) : convert(from, enableLegacySpecWorkaround);
			final String c1 = treatAsMills ? convertNative(to, enableLegacySpecWorkaround) : convert(to, enableLegacySpecWorkaround);
			this.builder.append(c0 + " --> " + c1);
			if (subtitle.hasPositionalData() && !disablePositioning) {
				final int h = subtitle.positionH();
				final int v = subtitle.positionV();
				if (v != -1) {
					this.builder.append(" line:" + (USE_OLD_POS_LOGIC ? (100 - v) : v) + "%");
				}
				if (h != -1) {
					this.builder.append(" position:" + (USE_OLD_POS_LOGIC ? (100 - h) : h) + "%");
				}
				if (h >= 74 && !USE_OLD_POS_LOGIC && ENABLE_POSITIONAL_TWEAKS) {
					this.builder.append(" align:right");
				} else if (h <= 24 && !USE_OLD_POS_LOGIC && ENABLE_POSITIONAL_TWEAKS) {
					this.builder.append(" align:left");
				}
				this.builder.append("\n");
			} else {
				this.builder.append("\n");
			}
			final String[] lines = Splitters.splitString(text, "\n");
			for (final String line : lines) {
				this.builder.append(StringsUtil.unescapeHtml3(line) + "\n");
			}
			this.builder.append("\n");
			this.index += 1;
			return null;
		});
		APPEND_ACCELERATOR.asyncSubmit(f).waitFor(60, TimeUnit.SECONDS);
	}

	public void addAll(final List<Subtitle> subtitles) {
		if (subtitles == null || subtitles.isEmpty())
			return;
		final Executable<?> f = ExecutorManager.make(() -> {
			for (final Subtitle subtitle : subtitles) {
				if (subtitle.hasStyles()) {
					if (subtitle.getQualifiyingStyles() > 0) {
						if (!this.hasStyles) {
							this.builder.append("STYLE\n");
							this.hasStyles = true;
							this.ignoreStyleAppend = true;
						} else {
							for (final SubtitleStyle style : subtitle.getStyles()) {
								if (!style.qualifies())
									continue;
								else {
									this.builder.append("::cue(." + style.textIdf + "), c[class=\"" + style.textIdf + "\"] {\n");
									if (style.hasColor()) {
										this.builder.append("color: #" + MathUtil.toHex(style.color) + ";\n");
									}
									if (style.hasBackgroundAlpha()) {
										if (style.backgroundAlpha == 0) {
											this.builder.append("background-color: transparent;\n");
										} else if (style.backgroundAlpha == 254 || style.backgroundAlpha == 255) {
										} else
											log.warn("Unsupported backgroundAlpha: " + style.backgroundAlpha);
									}
									if (style.hasForegroundAlpha()) {
										this.builder.append("opacity: " + (float) ((float) style.foregroundAlpha / 254) + ";\n");
									}
									if (style.hasTextSizeIncrement()) {
										this.builder.append("font-size: " + (float) (100 * style.textSizeIncrement) + "%;\n");
									}
									if (style.hasEdgeColor()) {
										final String color = "#" + MathUtil.toHex(style.edgeColor);
										this.builder.append("text-shadow: -1.1px -1.1px 0 " + color + ", 1.1px -1.1px 0 " + color + ", -1.1px 1.1px 0 " + color
												+ ", 1.1px 1.1px 0 " + color + ";\n");
									}
									if (style.hasFontFamily()) {
										if (style.fontFamily == 7)
											this.builder.append("font-variant: small-caps;\n");
										this.builder.append("font-family: " + SubtitleStyle.getFontFamilyForInt(style.fontFamily) + ";\n");
									}
									this.builder.append("}\n");
								}
							}
						}
					}
				}
			}
			if (this.hasStyles && this.ignoreStyleAppend)
				this.builder.append("\n");
			for (final Subtitle subtitle : subtitles) {
				this.add(subtitle);
			}
			return null;
		});
		APPEND_ACCELERATOR.asyncSubmit(f).waitFor(60, TimeUnit.SECONDS);
	}

	public String build() {
		return this.builder.toString();
	}

	private String convert(final float seconds, final boolean useWorkaround) {
		return TimeUtil.milliToString((long) (seconds * 1000), useWorkaround);
	}

	private String convertNative(final float seconds, final boolean useWorkaround) {
		return TimeUtil.milliToString((long) seconds, useWorkaround);
	}
}
